﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Threading.Tasks;
using CRM.Interface;
using Microsoft.AspNetCore.Http;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.ChangeTracking;
using Microsoft.Extensions.Primitives;

namespace CRM.Extensions
{
    public static class DbSetExtension
    {
        public static Task<PaginateStruct<T>> ToPaginateAsync<T>(this IQueryable<T> dbSet, HttpRequest request)
            where T : class
        {
            if (request == null) throw new ArgumentNullException(nameof(request));

            request.TryGetValue("page", out var page);
            request.TryGetValue("limit", out var limit);

            var intPage = Convert.ToInt32(page);
            intPage = intPage < 1 ? 1 : intPage;
            var intLimit = Convert.ToInt32(limit);
            return dbSet.ToPaginateAsync(intPage, intLimit);
        }

        public static async Task<PaginateStruct<T>> ToPaginateAsync<T>(this IQueryable<T> dbSet, int page = 1,
            int limit = 20) where T : class
        {
            var count = await dbSet.CountAsync();
            if (count == 0)
            {
                return new PaginateStruct<T>() {CurrentPage = 1, PageSize = limit, Items = new List<T>()};
            }

            limit = limit < 10 ? 10 : limit;
            var maxPage = (int) Math.Ceiling((double) count / limit);
            var currentPage = page > maxPage ? maxPage : (page < 1 ? 1 : page);

            return new PaginateStruct<T>()
            {
                Total = count,
                CurrentPage = currentPage,
                PageSize = limit,
                Items = await dbSet.Skip((currentPage - 1) * limit).Take(limit).ToListAsync()
            };
        }
        
        public static Task<PaginateStruct<T>> ToFakePaginateAsync<T>(this IQueryable<T> dbSet, HttpRequest request, int ratio = 0)
            where T : class
        {
            if (request == null) throw new ArgumentNullException(nameof(request));

            request.TryGetValue("page", out var page);
            request.TryGetValue("limit", out var limit);

            var intPage = Convert.ToInt32(page);
            intPage = intPage < 1 ? 1 : intPage;
            var intLimit = Convert.ToInt32(limit);
            return dbSet.ToFakePaginateAsync(intPage, intLimit, ratio);
        }
        
        public static async Task<PaginateStruct<T>> ToFakePaginateAsync<T>(this IQueryable<T> dbSet, int page = 1,
            int limit = 20, int ratio = 0) where T : class
        {
            if (ratio <= 0)
            {
                return await dbSet.ToPaginateAsync(page, limit);
            }
            var count = await dbSet.CountAsync();
            PaginateStruct<T> result;
            if (count / limit > page - 1)
            {
                result = await dbSet.ToPaginateAsync(page, limit);
            }
            else
            {
                limit = limit < 10 ? 10 : limit;
                var maxPage = (int) Math.Ceiling((double) count / limit);
                var currentPage = page > maxPage ? maxPage : (page < 1 ? 1 : page);
                result = new PaginateStruct<T>()
                {
                    Total = count,
                    CurrentPage = currentPage,
                    PageSize = limit,
                    Items = await dbSet.Skip((currentPage - 1) * limit).Take(limit).ToListAsync()
                };
            }

            result.Total = count * (ratio + 1);
            return result;
        }
        
        public static PaginateStruct<T> ToPaginate<T>(this IEnumerable<T> dbSet, HttpRequest request)
            where T : class
        {
            var page = StringValues.Empty;
            var limit = StringValues.Empty;
            if (request != null)
            {
                request.Query.TryGetValue("page", out page);
                request.Query.TryGetValue("limit", out limit);
            }

            var intPage = Convert.ToInt32(page);
            intPage = intPage < 1 ? 1 : intPage;
            var intLimit = Convert.ToInt32(limit);
            intLimit = intLimit < 1 ? 1 : intLimit;
            return dbSet.ToPaginate(intPage, intLimit);
        }

        public static PaginateStruct<T> ToPaginate<T>(this IEnumerable<T> dbSet, int page = 1,
            int limit = 20) where T : class
        {
            var enumerable = dbSet.ToList();
            var count = enumerable.Count;
            if (count == 0)
            {
                return new PaginateStruct<T>() {CurrentPage = 1, PageSize = limit, Items = new List<T>()};
            }

            limit = limit < 10 ? 10 : limit;
            var maxPage = (int) Math.Ceiling((double) count / limit);
            var currentPage = page > maxPage ? maxPage : (page < 1 ? 1 : page);

            return new PaginateStruct<T>()
            {
                Total = count,
                CurrentPage = currentPage,
                PageSize = limit,
                Items = enumerable.Skip((currentPage - 1) * limit).Take(limit).ToList()
            };
        }

        public class PaginateStruct<T> where T : class
        {
            public int Total { get; set; }
            public int CurrentPage { get; set; }
            public int PageSize { get; init; }
            public List<T> Items { get; init; }

            public PaginateStruct<TO> Transform<TO>(Func<T, TO> trans) where TO : class
            {
                return new()
                {
                    Total = Total,
                    CurrentPage = CurrentPage,
                    PageSize = PageSize,
                    Items = Items.Select(trans).ToList(),
                };
            }
        }


        public static void SoftRemove<TEntity>(this IQueryable<TEntity> dbSet, TEntity entity)
            where TEntity : ISoftDelete
        {
            entity.IsDeleted = true;
        }

        public static void Restore<TEntity>(this IQueryable<TEntity> dbSet, TEntity entity) where TEntity : ISoftDelete
        {
            entity.IsDeleted = false;
        }

        public static void ForceRemove<TEntity>(this DbSet<TEntity> dbSet, TEntity entity)
            where TEntity : class, ISoftDelete
        {
            dbSet.Remove(entity);
        }

        /// <summary>
        /// 根据当条件为真时添加where语句
        /// </summary>
        /// <param name="dbSet"></param>
        /// <param name="condition"></param>
        /// <param name="predicate"></param>
        /// <typeparam name="TSource"></typeparam>
        /// <returns></returns>
        public static IQueryable<TSource> WhenWhere<TSource>(this IQueryable<TSource> dbSet, bool condition,
            Expression<Func<TSource, bool>> predicate) where TSource : class
        {
            return condition ? dbSet.Where(predicate) : dbSet;
        }

        public static IEnumerable<TSource> WhenWhere<TSource>(this IEnumerable<TSource> dbSet, bool condition,
            Func<TSource, bool> predicate) where TSource : class
        {
            return condition ? dbSet.Where(predicate) : dbSet;
        }

        private static Dictionary<Type, object> _primaryKeyDic = new();

        public static IQueryable<TSource> WhereIn<TSource, TId>(this IQueryable<TSource> dbSet, TId[] ids)
            where TSource : class
        {
            Func<TSource, TId> lam;
            if (!_primaryKeyDic.ContainsKey(typeof(TSource)))
            {
                var key = (dbSet as DbSet<TSource>).EntityType.FindPrimaryKey().Properties.First().Name;
                var property = typeof(TSource).GetProperty(key);
                var paramSource = Expression.Parameter(typeof(TSource));
                var val = Expression.Call(Expression.Constant(property), "GetValue", null, paramSource);
                var intVal = Expression.Call(null, typeof(Convert).GetMethod("ToInt32", new[] {typeof(object)}), val);

                var res = Expression.Lambda<Func<TSource, TId>>(intVal, paramSource);
                lam = res.Compile();
                _primaryKeyDic.Add(typeof(TSource), lam);
            }
            else
            {
                lam = _primaryKeyDic[typeof(TSource)] as Func<TSource, TId>;
            }


            return dbSet.Where(m => ids.Contains(lam(m)));
        }
#if NET5_0       
        /// <summary>
        /// 去掉重复的key
        /// </summary>
        /// <param name="source"></param>
        /// <param name="keySelector"></param>
        /// <typeparam name="TSource"></typeparam>
        /// <typeparam name="TKey"></typeparam>
        /// <returns></returns>
        public static IEnumerable<TSource> DistinctBy<TSource, TKey>(this IEnumerable<TSource> source, Func<TSource, TKey> keySelector)
        {
            var seenKeys = new HashSet<TKey>();
            foreach (var element in source)
            {
                if (seenKeys.Add(keySelector(element)))
                {
                    yield return element;
                }
            }
        }
        

        public static IEnumerable<TSource[]> Chunk<TSource>(this IEnumerable<TSource> source, int size)
        {
            if (source == null)
            {
                throw new ArgumentNullException(nameof(source));
            }
            if (size < 1)
            {
                throw new ArgumentOutOfRangeException(nameof(size));
            }
            return ChunkIterator<TSource>(source, size);
        }

        private static IEnumerable<TSource[]> ChunkIterator<TSource>(IEnumerable<TSource> source, int size) where TSource : notnull
        {
            using (IEnumerator<TSource> e = source.GetEnumerator())
            {
                while (e.MoveNext())
                {
                    TSource[] array = new TSource[size];
                    array[0] = e.Current;
                    for (int i = 1; i < size; i++)
                    {
                        if (!e.MoveNext())
                        {
                            Array.Resize(ref array, i);
                            yield return array;
                            yield break;
                        }
                        array[i] = e.Current;
                    }
                    yield return array;
                }
            }
        }
#endif   
        public static IEnumerable<TSource> Shuffle<TSource>(this IEnumerable<TSource> sources)
        {
            var rnd = new Random();
            return sources.OrderBy(m => rnd.Next());
        }

        public static void Increment<TSource, TKey>(this EntityEntry<TSource> source, Expression<Func<TSource, TKey>> key, TKey num) where TSource : class
        {
            var table = source.Metadata.GetTableName();
            var expression = (MemberExpression)key.Body;
            string field = expression.Member.Name;
            var idStr = source.Metadata.FindPrimaryKey().Properties.Select(x => x.Name).Single();
            var id = source.Property(idStr).CurrentValue;
            source.Context.Database.ExecuteSqlRaw($"update `{table}` as `e` set `e`.`{field}`=`e`.`{field}`+{num}  where `e`.`{idStr}`={id}");
        }
    }
}